学习之前收集的资料

前言:什么是Cache Components

我在学习nexjts15的时候,好像没有听说过Cache Components,只见到过"use cache"这个命令,只知道有缓存功能。

Next.js 16(2025 年 10 月 21 日正式发布,目前最新小版本为 16.1.x)引入了 Cache Components,这是 App Router 缓存模型的一次重大变革。核心目标是让缓存行为完全显式、可预测,彻底告别之前版本中“隐式缓存规则太多、容易踩坑”的痛点。

开启方式(必须先做这一步)

在 next.config.ts / next.config.js 中设置:

开启后,整个缓存语义会发生根本性翻转:

特性Next.js 14 ~ 15(默认行为)Next.js 16 + cacheComponents: true
默认渲染模式尽可能静态(fetch 很容易被缓存)全部动态(request time 执行,除非显式缓存)
fetch() 是否自动缓存是(Data Cache)否(必须用 use cache 包裹才缓存)
cookies()/headers() 影响范围容易污染整条 Route 变成 dynamic只影响当前组件/函数,不会向上污染
想缓存某部分怎么做?很麻烦(force-static、unstable_cache 等 hack)直接加 'use cache' 指令,超级直观
静态 + 动态混合依赖实验性 PPR,边界模糊原生支持优秀(Partial Prerendering + Streaming)
心智负担高(规则隐晦、容易出意外)大幅降低(你要缓存就写出来,不写就是动态)

核心指令:'use cache'

'use cache' 是最关键的指令,可以加在三种地方:

1、文件顶部(整页/整布局缓存)

2、组件内部(组件级缓存)

3、工具函数(函数级缓存,最推荐)

控制缓存时长:cacheLife

常见内置 profile:

缓存失效:cacheTag + revalidateTag / updateTag

推荐的使用心法(生产项目常见做法)

1、默认保持动态 → 先把 cacheComponents: true 打开,然后尽量别加 'use cache',让页面默认走 request time + streaming,这样最安全、最符合直觉。

2、只对真正不变的部分加缓存 适合缓存的典型场景:

3、动态内容坚决不碰

4、经典组合:静态壳 + 动态流式内容

总结一句话

Next.js 16 的 Cache Components 把缓存决策权彻底还给了开发者:默认全部动态,需要缓存就显式写 'use cache' 这套模型配合 Partial Prerendering(PPR),能轻松实现“静态壳瞬间加载 + 动态内容流式渲染”,很多项目 TTFB 能下降 60%~80%。

老师讲解

Summary of Caching Without Cache Components

创建项目

直接copy老师的代码,main分支即可。然后编写.env文件。

安装依赖:npm installnpm install postgres

配置drizzle:

在第一次启动前,你需要同步数据库结构:

  1. 生成迁移文件(将代码中的 Schema 转换为 SQL):

  2. 执行迁移(将结构应用到真实的数据库中):

  3. (可选)填充数据:如果项目提供了一些初始测试数据,可以运行:

npm run dev项目运行成功。

nextjs 16之前的缓存总结

在nextjs 16之前,推荐在server components里面请求数据,因为更加接近服务端,请求会更快,而且nextjs提供的缓存功能。

在开发环境,是每次刷新都会请求数据:

但是放到生产环境,nextjs会将这个页面当作静态页面,里面的请求也只会执行一次。

npm run build

image-20251229100425055

运行:npm run start,可以看到,只会在第一次发起请求,之后都不会了。

这个接口就是一个例子,虽然接口和接口参数都没有改变,但是实际上它的返回结果会不同,这时候nextjs的缓存反而是一种阻碍。

怎么样才能动态请求呢?参考:https://nextjs.org/docs/app/guides/caching#dynamic-rendering,有下面几种方法:

image-20251229104332717

这就是nextjs v16之前,caching相关的概念。

Cache Components First Look

启用cacheComponents

为了开启nextjs v16中的cache components功能,需要在next.config.ts中配置cacheComponents: true

启用之后会发生什么

开启 cacheComponents(Next.js 15 末期 → 16+ 的实验/正式功能)之后,Next.js App Router 的默认行为发生了非常大的哲学转变,和之前版本(尤其是 Next.js 14 和早期 15)完全相反。

核心一句话总结:

开启 cacheComponents 后,默认一切数据获取都是动态的(运行时执行),除非你主动用 'use cache' 把某部分标记为要缓存。

默认行为详细对比(开启 cacheComponents 前 vs 后)

方面开启前(Next.js 14 ~ 15早期 / PPR 未完全稳定时)开启 cacheComponents 后(Next.js 16+ 推荐模式)变化方向
fetch() 默认缓存是(force-cache)否(相当于 no-store)从默认缓存 → 默认不缓存
非 fetch 数据源(数据库、第三方库)通常不缓存,除非用 React.cache 或 unstable_cache完全不缓存,必须显式用 'use cache'更严格
整个路由的渲染倾向静态(build 时尽可能生成)默认动态(请求时渲染),静态壳 + 动态洞更动态、更安全
Partial Prerendering实验性,需要单独开启默认行为(静态壳 + 流式动态内容)变成默认
要缓存某部分比较难,需要各种 opts 或 force-static简单:加 'use cache' + 可选 cacheLife()显式、细粒度
cookies/headers/searchParams让整条路由变动态仍然让所在组件变动态,但可以包在 更可组合
构建时能生成多少静态内容尽可能多很少(只有纯计算、静态 JSX),数据部分默认被排除构建更快、更安全

实际开发中最常遇到的默认表现

↑ 这段代码在开启 cacheComponents 后,每次请求都会重新执行数据库查询和 fetch。

想让它缓存?必须显式写:

总结:开启 cacheComponents 后的“新默认哲学”

一句话记住:

以前:想动态要特意 opt-out 现在(cacheComponents 开启后):想缓存才特意 opt-in

核心概念

Next.js App Router 中 Partial Prerendering (PPR) 的核心实现方式,目标是:让同一个路由里同时拥有极快的静态壳 + 可缓存的中频数据 + 实时动态内容,避免过去“要么全静要么全动”的两难选择。

原理:

在构建/预渲染时,Next.js 会尽量把页面渲染成一个静态 HTML 壳(static shell),里面包含所有能提前算完的内容。 真正动态/请求相关/网络请求的内容,则被推迟到请求时刻再渲染(通过 Suspense 流式传输)。

一句话总结:

用 use cache 把能共享、变化不频繁的数据拉进静态壳, 用 Suspense 把每个用户、每次请求都不同的内容推到流式渲染, 就能在同一个页面里同时获得 极致的首屏速度 + 合理的实时性。

注意:use cache 和 Suspense 之间,没有强烈的关系。用了use cache,可能需要使用Suspense进行包裹,也可能不需要,关键是看有没有“运行时数据”,这一点要搞清楚。

默认行为

情况是否进入静态壳处理方式
纯计算、模块导入、同步文件读取是(自动)直接包含在静态 HTML 中
fetch、数据库查询、异步操作否(默认)必须显式处理,否则开发/构建报错
使用 cookies/headers/searchParams否(永远)只能放在 <Suspense>
随机值 (Math.random()) 等否(非确定性)必须推迟或用 "use cache"

报错提示(很常见):

如何控制缓存?(三种主要手段)

手段用途代码示例适用场景
use cache主动把动态数据拉进静态壳'use cache' cacheLife('hours')中频更新、共享数据(如文章、商品列表)
<Suspense>把内容推迟到请求时渲染<Suspense fallback={<Loading/>}>个性化、实时数据、cookies 相关
不处理(默认)保持动态,每次请求都重新算高度个性化、非常频繁变化的内容

开启了cacheComponents之后,默认情况就是动态请求。

什么情况下加Suspense包裹?一般来说,可以直接编写,如果nextjs要求你添加,它会报错。可以看一下这里:https://nextjs.org/docs/app/getting-started/cache-components#putting-it-all-together。但是呢,还是总结一下:

什么时候可以不用 Suspense?(推荐尽可能不包)

这种组件会直接被包含在静态 shell里,成为页面最快出现的部分。

什么是运行时数据?

运行时数据 = 只有在用户真正请求页面时(浏览器访问的那一刻),才能拿到或计算出来的数据。

它和“构建时/预渲染时就能确定的数据”形成鲜明对比。

运行时数据 vs 构建时数据(对比表)

特性构建时数据(Build-time / Pre-rendered)运行时数据(Runtime)
什么时候确定值构建/部署时(next build 或 Vercel 部署时)用户请求页面时(每次访问都可能不同)
是否能进入静态 HTML 壳是(直接包含在生成的 HTML 里)否(必须推迟到请求时渲染,通常通过 Suspense)
缓存情况(默认)可以被缓存('use cache' 或默认静态)默认不缓存(每次都重新计算)
常见触发条件纯计算、静态内容、'use cache' 的数据cookies、headers、searchParams、当前时间、用户数据
典型代表文章正文、商品基本信息(不依赖登录)、分类列表用户昵称、购物车数量、实时库存、个性化推荐
常见的运行时数据例子(几乎每次请求都会不同)
  1. cookies() / headers() 相关

  2. searchParams(URL 查询参数)

  3. 当前登录用户数据

  4. 实时/时间敏感数据

  5. 数据库查询 + 用户相关过滤

  6. 外部 API 调用 + 带 token/认证

在 Next.js 现代模式(cacheComponents 开启)下的处理方式

一句话总结

运行时数据 = “用户一进来就知道不一样”的数据 它包括:

常用缓存控制 API(Next.js 16+ 新增)

实际组合模式(最常用写法)

迁移旧项目时要注意(Next.js 15 → 16+ 常见坑)

问题

如果一个组件,比如说header、footer、nav等,里面的代码都是静态代码,还需要特地指定 'use cache' 吗?

 

在 Next.js 16 中(开启 cacheComponents: true 的情况下),对于完全静态的组件(header、footer、nav 等),里面没有任何动态操作(无 fetch、无 cookies、headers、searchParams、无数据库查询等),你通常已经不需要特地加 'use cache' 了。

如果不加不放心,可以查看waterfall、TTFB,这些到时候一查就会了。

比较健康的 TTFB 参考值(生产环境,75 分位):

TTFB 值评价建议
< 200ms非常优秀静态/边缘缓存/好的服务器
200~500ms正常/可接受大部分 Next.js 项目在这个区间
500~800ms需要关注可能有动态渲染或冷启动问题
> 800ms明显拖慢体验必须优化(PPR、cache、ISR 等)

下面是目前(2026年1月)Next.js 16 的实际情况对比表,方便理解:

目前社区最常见的实际做法(2025年底~2026年初)

什么时候你应该'use cache' 到 header/footer 上?

  1. 你发现打开页面时 header/footer 区域竟然在 streaming(出现 loading)→ 加 'use cache' 强制纳入静态壳
  2. 你开启了 cacheComponents: true,但发现某些纯静态组件在动态路由里没有被很好地复用
  3. 项目团队编码规范要求「所有可缓存组件一律显式声明」
  4. 你在做 Partial Prerendering (PPR) 的极致优化,想确保任何情况下 header/footer 都是静态的

 

Partial Pre-rendering, Static and Dynamic Components

partial pre rendering的概念

https://nextjs.org/docs/app/getting-started/cache-components#how-rendering-works-with-cache-components

image-20251229160742905

核心概念

在构建/预渲染阶段,Next.js 会尽可能生成一个静态 HTML 壳(static shell),里面包含所有能提前算完的 UI 和内容。 真正需要请求时才能确定或每次都不同的动态部分,则被推迟到用户实际访问时再渲染,并通过流式传输(streaming)逐步填充到静态壳中。结果:用户几乎瞬间看到页面框架(通常 <100ms),动态内容随后流式出现,既快又新鲜。

它是怎么工作的?

  1. 预渲染阶段(build time 或首次请求时) Next.js 从上到下遍历组件树:

    • 能同步完成的部分(纯 JSX、import、fs.readFileSync、纯计算等)→ 直接渲染进静态 HTML 壳

    • 遇到无法完成的部分(fetch、数据库查询、cookies()、headers()、searchParams、Date.now()、Math.random() 等)→ 必须显式处理:

      • 包裹 → fallback 进入静态壳,真实内容推迟到请求时流式渲染
      • 或者用 'use cache' 标记 → 如果不依赖运行时数据,就把结果缓存起来,塞进静态壳
  2. 响应阶段(用户访问时)

    • 浏览器立即收到完整的静态 HTML 壳 + RSC payload(React Server Component 序列化数据)
    • 动态部分(Suspense 洞)通过 React 的 streaming 逐步替换 fallback
  3. 客户端导航(点击链接跳转)

    • 利用 RSC payload + Activity API 实现接近瞬间的导航(隐藏路由保持状态)

static components & dynamic components

在 Next.js 16 中(开启 Cache Components 后),已经不再像以前那样明确区分「static components」和「dynamic components」这两个类别了。

取而代之的是一个更细粒度、更自动化的混合模型:Partial Prerendering (PPR) + Cache Components,核心思想变成了:

“页面默认是动态的,但 Next.js 会尽量自动提取出能预渲染的部分,生成一个静态 HTML shell,剩下的部分通过 Suspense 变成动态洞(holes),或者用 'use cache' 主动拉回静态 shell。”

Next.js 16 实际运作的“三种内容”分类(而不是两种组件)

  1. Static Shell 部分(自动的静态内容)

    • 纯 JSX、同步计算、模块导入等
    • 没有数据获取、没有运行时 API(cookies/headers/searchParams)
    • 直接包含在预渲染的 HTML 中,最快出现
  2. Cached Content 部分(主动缓存的“曾经动态”内容)

    • 用 'use cache' 标记的组件/函数
    • 可以有 fetch、数据库查询,但不能依赖运行时数据
    • 构建时/首次请求时执行一次,之后复用 → 可以进入静态 shell 或被缓存复用
  3. Dynamic Holes / Runtime 部分(真正的运行时内容)

    • 必须用 <Suspense> 包裹
    • 包含 cookies()、headers()、searchParams、Date.now()、Math.random() 等
    • 每次请求都重新执行,通过 streaming 逐步替换 fallback

使用use cache可以缓存函数、组件或者文件。

image-20251229162042241

老师案例

重点就是,Dynamic Holes / Runtime 部分必须使用Suspense进行包裹;使用use cache来缓存内容。

可以看到,只有Static random word是静态内容,其余都是动态的。

Product Page Example

接下来的三节课,都使用products/[id]/page.tsx这个页面来说明各种cache规则到底应该在哪里做、怎么做。

先构建页面,并将数据请求的函数放到utils.ts文件里面。

分析:

产品的title和description基本不变,所以可以做缓存。而price的变化也不是太频繁,但是会变,所以单独使用一种cache策略use cahce: remote,下节课会将。recommendedProducts这个就经常变化了,开启了cacheComponents之后,可以不处理。

这节课先解决title和description的缓存问题。

添加缓存

因为title和description都是从getProduct这个请求接口返回的,所以直接在这个函数里面使用use cache即可。

image-20251229190916967

可以看到,页面有一个报错:

image-20251229191357880

报错原因:

在 页面顶层(Page 组件) 直接 await params 和 await getProduct(+id),而 Next.js 16(开启 Cache Components / Partial Prerendering 后)把这个行为视为访问了运行时不确定数据。

在动态路由 [id] 中:

结果:Next.js 故意抛这个错误来强制你做出选择:

老师是使用generateStaticParams来解决。设置一些静态页面。

generateStaticParams 是 Next.js 的官方 API,专门用于 App Router(app 目录)下的动态路由(dynamic routes),它的作用是在构建时(build time)提前告诉 Next.js 要为哪些动态参数生成静态页面。

可以看到,正常了,渲染速度是非常快的。

 

grok的解决办法

把 await params 和初始数据获取下沉到子组件里,也就是将 params 作为 Promise 向下传递 → 在子组件里 await,并用 Suspense 包裹:

效果我反正看不出区别。

Remote Cache

image-20251229184839942

虽然 use cache 指令对于大多数应用需求已经足够,但你偶尔可能会注意到,缓存的操作比预期更频繁地重新执行,或者你的上游服务(CMS、数据库、外部 API)收到的请求比你预期的要多。这是因为内存内缓存(in-memory caching)存在固有的局限性:

需要注意的是,use cache 提供的价值远超单纯的服务器端缓存:它会告知 Next.js 哪些内容可以被预获取(prefetch),并为客户端导航定义过时时间(stale times)。

'use cache: remote' 指令允许你以声明式的方式指定,将缓存的输出存储在远程缓存中,而不是内存中。虽然这为特定操作提供了更持久的缓存,但也伴随着权衡取舍:基础设施成本以及缓存查找时的网络延迟。

强烈推荐使用 'use cache: remote' 的场景:

老师案例

price采用这种缓存方式。获取price单独创建一个接口,然后使用use cache: remote来做缓存。

image-20251229194607631

然后为price单独做一个组件,使用Suspense来包裹:

image-20251229194709233

connection的意思:“我故意告诉 Next.js:别把我放进静态壳里,我要在每次请求时都重新跑一遍!”它是一个调试/教学/强制动态的小技巧,而不是正常业务代码中应该长期保留的东西。

可以看到,price只在第一次请求了数据,然后就缓存了数据。

Private Cache

image-20251229194923211

这是一个实验性的命令。

'use cache: private' 指令允许函数在缓存作用域内直接访问运行时请求 API,例如 cookies()headers()searchParams 然而,结果永远不会存储在服务器上,它们仅在浏览器内存中缓存,并且在页面重新加载后不会持久存在。

何时使用 'use cache: private'

由于这个指令会访问运行时数据,因此该函数会在每次服务器渲染时都执行,并且在静态壳(static shell)生成阶段被完全排除,不会参与预渲染。无法为 'use cache: private' 配置自定义缓存处理器(custom cache handlers)。

老师案例

推荐product列表使用了session来获取,是针对不同用户有不同的推荐列表。那么同一个用户,在不同product详情里面,展示的推荐列表应该是一致的,所以这些数据应该被缓存。但是它又不是一直不变,我们需要的是缓存时间稍微折中一点,所以用上了这个命令:use cache: private

接口获取函数里面使用use cache: private

image-20251229200527739

然后将相应的逻辑提取出来,单独做一个组件:

image-20251229200459825

 

可以看到,不同的详情页面,推荐列表只请求了一次,其余的情况都使用缓存。

cacheLife & revalidatePath

核心对比表

特性cacheLiferevalidatePath
作用对象单个函数/组件(Cache Component)整条路径(route/path)
控制粒度非常细(具体到某个数据查询)比较粗(整个页面或路由段)
主要目的设置这个缓存多长时间后应该重新生成/验证手动使某条路径的缓存失效(立即或延迟)
使用位置函数/组件内部,搭配 'use cache'Server Action、Route Handler、Server Component
生效时机构建时/首次请求时 + 后续 revalidate 时人为触发时(按钮点击、表单提交等)
是否立即生效否(等到时间到期或手动触发)可以立即(revalidatePath(path, 'page'))
典型场景商品详情、文章内容、分类列表(中频更新)用户提交评论后刷新文章页、修改配置后刷新设置页
是否会影响静态壳是(会决定是否能长期进入预渲染)是(会使整条路径重新生成静态壳)
当前状态稳定推荐稳定,但逐渐被 revalidateTag 部分取代

1、cacheLife —— “这个数据能活多久”

常见内置快捷方式(推荐优先使用):

典型用法:

效果: 这个函数的结果会在1小时内被复用,即使在不同用户、不同 serverless 实例之间(如果用了 use cache: remote 则更可靠)。 超过 1 小时后,下次请求会重新执行(background revalidation)。

2、revalidatePath —— “我现在就要你重新生成!”

用途:用户刚修改了数据,我希望页面立刻更新;我想在操作完成后刷新整个页面;我想让多个页面同时失效(比如所有文章)。

第二个参数选项(很重要!):

选项含义影响范围推荐场景
'page'只失效当前 path 的页面最小(最精准)编辑单个商品后刷新详情页
'layout'失效包含这个 path 的所有 layout + page中等修改全局导航/侧边栏后刷新
无(默认)相当于 'page'同 'page'大多数情况

cacheTag, revalidateTag & updateTag

cacheTagrevalidateTagupdateTag 是 Next.js 16+(Cache Components 体系)中非常强大且推荐的缓存失效机制,它们构成了目前最灵活、最细粒度的标签式缓存管理方式。

下面用最清晰的对比和实际场景来讲解这三个 API 的作用、区别和推荐用法

三者快速对比

API作用时机作用对象粒度典型使用位置是否立即生效是否实验性
cacheTag定义时给某个缓存打标签非常细'use cache' 函数内部稳定
revalidateTag运行时触发使打过这个 tag 的所有缓存失效细到中等Server Action / Route Handler是(可配置延迟)稳定
updateTag运行时触发立即使打过这个 tag 的缓存失效细到中等Server Action立即实验性

1、cacheTag(tagName)

作用:给当前这个 'use cache' 的函数/组件打上一个或多个标签(tag),方便后续批量失效。

语法(通常和 cacheLife 一起用):

效果: 这个函数的缓存结果会被关联到 products、product-123、expensive-queries 这三个标签。 后续只要失效其中任何一个标签,这个缓存就会被重新生成。

2、revalidateTag(tagName, options?)

作用: 手动触发:让所有打了这个 tag 的缓存失效(下次请求时重新执行)。

常用位置:Server Action、Route Handler

语法:

revalidatePath 的最大区别:

推荐优先级(2025 年底社区共识):

3、updateTag(tagName)

作用: 立即使打了这个 tag 的所有缓存失效,并且在当前请求中就使用最新的数据(不像 revalidateTag 是“下次请求再更新”)。

语法(目前仍实验性):

关键区别:

适用场景:

推荐的现代组合模式

总结:三者的定位与选择指南